<?php

class Db_Adapter_Base {

  protected $_pdoType;
  /**
   * User-provided configuration
   *
   * @var array
   */
  protected $_config = array();
  /**
   * Fetch mode
   *
   * @var integer
   */
  protected $_fetchMode = Db::FETCH_ASSOC;
  /**
   * Query profiler object, of type Db_Profiler
   * or a subclass of that.
   *
   * @var Db_Profiler
   */
  protected $_profiler;
  /**
   * Default class name for a DB statement.
   *
   * @var string
   */
  protected $_defaultStmtClass = 'PDOStatement';
  //protected $_defaultStmtClass = 'PDO_Statement';
  /**
   * Default class name for the profiler object.
   *
   * @var string
   */
  
  protected $_defaultProfilerClass = 'Zend_Db_Profiler';
  /**
   * Database connection
   *
   * @var object|resource|null
   */
  protected $_connection = null;
  /**
   * Specifies the case of column names retrieved in queries
   * Options
   * Db::CASE_NATURAL (default)
   * Db::CASE_LOWER
   * Db::CASE_UPPER
   *
   * @var integer
   */
  protected $_caseFolding = Db::CASE_NATURAL;
  /**
   * Specifies whether the adapter automatically quotes identifiers.
   * If true, most SQL generated by Db classes applies
   * identifier quoting automatically.
   * If false, developer must quote identifiers themselves
   * by calling quoteIdentifier().
   *
   * @var bool
   */
  protected $_autoQuoteIdentifiers = true;
  /**
   * Keys are UPPERCASE SQL datatypes or the constants
   * Db::INT_TYPE, Db::BIGINT_TYPE, or Db::FLOAT_TYPE.
   *
   * Values are:
   * 0 = 32-bit integer
   * 1 = 64-bit integer
   * 2 = float or decimal
   *
   * @var array Associative array of datatypes to values 0, 1, or 2.
   */
  protected $_numericDataTypes = array(
      Db::INT_TYPE => Db::INT_TYPE,
      Db::BIGINT_TYPE => Db::BIGINT_TYPE,
      Db::FLOAT_TYPE => Db::FLOAT_TYPE
  );
  /** Weither or not that object can get serialized
   *
   * @var bool
   */
  protected $_allowSerialization = true;
  /**
   * Weither or not the database should be reconnected
   * to that adapter when waking up
   *
   * @var bool
   */
  protected $_autoReconnectOnUnserialize = false;

  /**
   * Constructor.
   *
   * $config is an array of key/value pairs or an instance of Zend_Config
   * containing configuration options.  These options are common to most adapters:
   *
   * dbname         => (string) The name of the database to user
   * username       => (string) Connect to the database as this username.
   * password       => (string) Password associated with the username.
   * host           => (string) What host to connect to, defaults to localhost
   *
   * Some options are used on a case-by-case basis by adapters:
   *
   * port           => (string) The port of the database
   * persistent     => (boolean) Whether to use a persistent connection or not, defaults to false
   * protocol       => (string) The network protocol, defaults to TCPIP
   * caseFolding    => (int) style of case-alteration used for identifiers
   *
   * @param  array|Zend_Config $config An array or instance of Zend_Config having configuration data
   * @throws Db_Adapter_Exception
   */
  public function __construct($config) {
    /*
     * Verify that adapter parameters are in an array.
     */
    if (!is_array($config)) {
      /*
       * Convert Zend_Config argument to a plain array.
       */
      if ($config instanceof Zend_Config) {
        $config = $config->toArray();
      } else {
        /**
         * @see Db_Adapter_Exception
         */
        require_once 'Zend/Db/Adapter/Exception.php';
        throw new Db_Adapter_Exception('Adapter parameters must be in an array or a Zend_Config object');
      }
    }

    $this->_checkRequiredOptions($config);

    $options = array(
        Db::CASE_FOLDING => $this->_caseFolding,
        Db::AUTO_QUOTE_IDENTIFIERS => $this->_autoQuoteIdentifiers,
        Db::FETCH_MODE => $this->_fetchMode,
    );
    $driverOptions = array();

    /*
     * normalize the config and merge it with the defaults
     */
    if (array_key_exists('options', $config)) {
      // can't use array_merge() because keys might be integers
      foreach ((array) $config['options'] as $key => $value) {
        $options[$key] = $value;
      }
    }
    if (array_key_exists('driver_options', $config)) {
      if (!empty($config['driver_options'])) {
        // can't use array_merge() because keys might be integers
        foreach ((array) $config['driver_options'] as $key => $value) {
          $driverOptions[$key] = $value;
        }
      }
    }

    if (!isset($config['charset'])) {
      $config['charset'] = null;
    }

    if (!isset($config['persistent'])) {
      $config['persistent'] = false;
    }

    $this->_config = array_merge($this->_config, $config);
    $this->_config['options'] = $options;
    $this->_config['driver_options'] = $driverOptions;


    // obtain the case setting, if there is one
    if (array_key_exists(Db::CASE_FOLDING, $options)) {
      $case = (int) $options[Db::CASE_FOLDING];
      switch ($case) {
        case Db::CASE_LOWER:
        case Db::CASE_UPPER:
        case Db::CASE_NATURAL:
          $this->_caseFolding = $case;
          break;
        default:
          /** @see Db_Adapter_Exception */
          require_once 'Zend/Db/Adapter/Exception.php';
          throw new Db_Adapter_Exception('Case must be one of the following constants: '
                  . 'Db::CASE_NATURAL, Db::CASE_LOWER, Db::CASE_UPPER');
      }
    }

    if (array_key_exists(Db::FETCH_MODE, $options)) {
      if (is_string($options[Db::FETCH_MODE])) {
        $constant = 'Db::FETCH_' . strtoupper($options[Db::FETCH_MODE]);
        if (defined($constant)) {
          $options[Db::FETCH_MODE] = constant($constant);
        }
      }
      $this->setFetchMode((int) $options[Db::FETCH_MODE]);
    }

    // obtain quoting property if there is one
    if (array_key_exists(Db::AUTO_QUOTE_IDENTIFIERS, $options)) {
      $this->_autoQuoteIdentifiers = (bool) $options[Db::AUTO_QUOTE_IDENTIFIERS];
    }

    // obtain allow serialization property if there is one
    if (array_key_exists(Db::ALLOW_SERIALIZATION, $options)) {
      $this->_allowSerialization = (bool) $options[Db::ALLOW_SERIALIZATION];
    }

    // obtain auto reconnect on unserialize property if there is one
    if (array_key_exists(Db::AUTO_RECONNECT_ON_UNSERIALIZE, $options)) {
      $this->_autoReconnectOnUnserialize = (bool) $options[Db::AUTO_RECONNECT_ON_UNSERIALIZE];
    }

    // create a profiler object
    $profiler = false;
    if (array_key_exists(Db::PROFILER, $this->_config)) {
      $profiler = $this->_config[Db::PROFILER];
      unset($this->_config[Db::PROFILER]);
    }
    $this->setProfiler($profiler);
  }

  /**
   * Check for config options that are mandatory.
   * Throw exceptions if any are missing.
   *
   * @param array $config
   * @throws Db_Adapter_Exception
   */
  protected function _checkRequiredOptions(array $config) {
    // we need at least a dbname
    if (!array_key_exists('dbname', $config)) {
      /** @see Db_Adapter_Exception */
      require_once 'Zend/Db/Adapter/Exception.php';
      throw new Db_Adapter_Exception("Configuration array must have a key for 'dbname' that names the database instance");
    }

    if (!array_key_exists('password', $config)) {
      /**
       * @see Db_Adapter_Exception
       */
      require_once 'Zend/Db/Adapter/Exception.php';
      throw new Db_Adapter_Exception("Configuration array must have a key for 'password' for login credentials");
    }

    if (!array_key_exists('username', $config)) {
      /**
       * @see Db_Adapter_Exception
       */
      require_once 'Zend/Db/Adapter/Exception.php';
      throw new Db_Adapter_Exception("Configuration array must have a key for 'username' for login credentials");
    }
  }

  /**
   * Returns the underlying database connection object or resource.
   * If not presently connected, this initiates the connection.
   *
   * @return object|resource|null
   */
  public function getConnection() {
    $this->_connect();
    return $this->_connection;
  }

  /**
   * Returns the configuration variables in this adapter.
   *
   * @return array
   */
  public function getConfig() {
    return $this->_config;
  }

  /**
   * Set the adapter's profiler object.
   *
   * The argument may be a boolean, an associative array, an instance of
   * Db_Profiler, or an instance of Zend_Config.
   *
   * A boolean argument sets the profiler to enabled if true, or disabled if
   * false.  The profiler class is the adapter's default profiler class,
   * Db_Profiler.
   *
   * An instance of Db_Profiler sets the adapter's instance to that
   * object.  The profiler is enabled and disabled separately.
   *
   * An associative array argument may contain any of the keys 'enabled',
   * 'class', and 'instance'. The 'enabled' and 'instance' keys correspond to the
   * boolean and object types documented above. The 'class' key is used to name a
   * class to use for a custom profiler. The class must be Db_Profiler or a
   * subclass. The class is instantiated with no constructor arguments. The 'class'
   * option is ignored when the 'instance' option is supplied.
   *
   * An object of type Zend_Config may contain the properties 'enabled', 'class', and
   * 'instance', just as if an associative array had been passed instead.
   *
   * @param  Db_Profiler|Zend_Config|array|boolean $profiler
   * @return Db_Adapter_Abstract Provides a fluent interface
   * @throws Db_Profiler_Exception if the object instance or class specified
   *         is not Db_Profiler or an extension of that class.
   */
  public function setProfiler($profiler) {
    $enabled = null;
    $profilerClass = $this->_defaultProfilerClass;
    $profilerInstance = null;

    if ($profilerIsObject = is_object($profiler)) {
      if ($profiler instanceof Db_Profiler) {
        $profilerInstance = $profiler;
      } else if ($profiler instanceof Zend_Config) {
        $profiler = $profiler->toArray();
      } else {
        /**
         * @see Db_Profiler_Exception
         */
        require_once 'Zend/Db/Profiler/Exception.php';
        throw new Db_Profiler_Exception('Profiler argument must be an instance of either Db_Profiler'
                . ' or Zend_Config when provided as an object');
      }
    }

    if (is_array($profiler)) {
      if (isset($profiler['enabled'])) {
        $enabled = (bool) $profiler['enabled'];
      }
      if (isset($profiler['class'])) {
        $profilerClass = $profiler['class'];
      }
      if (isset($profiler['instance'])) {
        $profilerInstance = $profiler['instance'];
      }
    } else if (!$profilerIsObject) {
      $enabled = (bool) $profiler;
    }

    if ($profilerInstance === null) {
      if (!class_exists($profilerClass)) {
        require_once 'Zend/Loader.php';
        Zend_Loader::loadClass($profilerClass);
      }
      $profilerInstance = new $profilerClass();
    }

    
    $this->_profiler = $profilerInstance;

    return $this;
  }

  /**
   * Returns the profiler for this adapter.
   *
   * @return Db_Profiler
   */
  public function getProfiler() {
    return $this->_profiler;
  }

  /**
   * Get the default statement class.
   *
   * @return string
   */
  public function getStatementClass() {
    return $this->_defaultStmtClass;
  }

  /**
   * Set the default statement class.
   *
   * @return Db_Adapter_Abstract Fluent interface
   */
  public function setStatementClass($class) {
    $this->_defaultStmtClass = $class;
    return $this;
  }

  /**
   * Prepares and executes an SQL statement with bound data.
   *
   * @param  mixed  $sql  The SQL statement with placeholders.
   *                      May be a string or Db_Select.
   * @param  mixed  $bind An array of data to bind to the placeholders.
   * @return Db_Statement_Interface
   */
  public function query($sql, $bind = array()) {
    // make sure $bind to an array;
    // don't use (array) typecasting because
    // because $bind may be a Db_Expr object
    if (!is_array($bind)) {
      $bind = array($bind);
    }
    
    // prepare and execute the statement with profiling
    $stmt = $this->prepare($sql);
    $stmt->execute($bind);

    // return the results embedded in the prepared statement object
    $stmt->setFetchMode($this->_fetchMode);
    return $stmt;
  }

  /**
   * Leave autocommit mode and begin a transaction.
   *
   * @return Db_Adapter_Abstract
   */
  public function beginTransaction() {
    $this->_connect();
    $q = $this->_profiler->queryStart('begin', Db_Profiler::TRANSACTION);
    $this->_beginTransaction();
    $this->_profiler->queryEnd($q);
    return $this;
  }

  /**
   * Commit a transaction and return to autocommit mode.
   *
   * @return Db_Adapter_Abstract
   */
  public function commit() {
    $this->_connect();
    $q = $this->_profiler->queryStart('commit', Db_Profiler::TRANSACTION);
    $this->_commit();
    $this->_profiler->queryEnd($q);
    return $this;
  }

  /**
   * Roll back a transaction and return to autocommit mode.
   *
   * @return Db_Adapter_Abstract
   */
  public function rollBack() {
    $this->_connect();
    $q = $this->_profiler->queryStart('rollback', Db_Profiler::TRANSACTION);
    $this->_rollBack();
    $this->_profiler->queryEnd($q);
    return $this;
  }

  /**
   * Inserts a table row with specified data.
   *
   * @param mixed $table The table to insert data into.
   * @param array $bind Column-value pairs.
   * @return int The number of affected rows.
   */
  public function insert($table, array $bind) {
    // extract and quote col names from the array keys
    $cols = array();
    $vals = array();
    $i = 0;
    foreach ($bind as $col => $val) {
      $cols[] = $this->quoteIdentifier($col, true);
      if ($val instanceof Db_Expr) {
        $vals[] = $val->__toString();
        unset($bind[$col]);
      } else {
        if ($this->supportsParameters('positional')) {
          $vals[] = '?';
        } else {
          if ($this->supportsParameters('named')) {
            unset($bind[$col]);
            $bind[':col' . $i] = $val;
            $vals[] = ':col' . $i;
            $i++;
          } else {
            /** @see Db_Adapter_Exception */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Db_Adapter_Exception(get_class($this) . " doesn't support positional or named binding");
          }
        }
      }
    }

    // build the statement
    $sql = "INSERT INTO "
            . $this->quoteIdentifier($table, true)
            . ' (' . implode(', ', $cols) . ') '
            . 'VALUES (' . implode(', ', $vals) . ')';

    // execute the statement and return the number of affected rows
    if ($this->supportsParameters('positional')) {
      $bind = array_values($bind);
    }
    $stmt = $this->query($sql, $bind);
    $result = $stmt->rowCount();
    return $result;
  }

  /**
   * Updates table rows with specified data based on a WHERE clause.
   *
   * @param  mixed        $table The table to update.
   * @param  array        $bind  Column-value pairs.
   * @param  mixed        $where UPDATE WHERE clause(s).
   * @return int          The number of affected rows.
   */
  public function update($table, array $bind, $where = '') {
    /**
     * Build "col = ?" pairs for the statement,
     * except for Db_Expr which is treated literally.
     */
    $set = array();
    $i = 0;
    foreach ($bind as $col => $val) {
      if ($val instanceof Db_Expr) {
        $val = $val->__toString();
        unset($bind[$col]);
      } else {
        if ($this->supportsParameters('positional')) {
          $val = '?';
        } else {
          if ($this->supportsParameters('named')) {
            unset($bind[$col]);
            $bind[':col' . $i] = $val;
            $val = ':col' . $i;
            $i++;
          } else {
            /** @see Db_Adapter_Exception */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Db_Adapter_Exception(get_class($this) . " doesn't support positional or named binding");
          }
        }
      }
      $set[] = $this->quoteIdentifier($col, true) . ' = ' . $val;
    }

    $where = $this->_whereExpr($where);

    /**
     * Build the UPDATE statement
     */
    $sql = "UPDATE "
            . $this->quoteIdentifier($table, true)
            . ' SET ' . implode(', ', $set)
            . (($where) ? " WHERE $where" : '');

    /**
     * Execute the statement and return the number of affected rows
     */
    if ($this->supportsParameters('positional')) {
      $stmt = $this->query($sql, array_values($bind));
    } else {
      $stmt = $this->query($sql, $bind);
    }
    $result = $stmt->rowCount();
    return $result;
  }

  /**
   * Deletes table rows based on a WHERE clause.
   *
   * @param  mixed        $table The table to update.
   * @param  mixed        $where DELETE WHERE clause(s).
   * @return int          The number of affected rows.
   */
  public function delete($table, $where = '') {
    $where = $this->_whereExpr($where);

    /**
     * Build the DELETE statement
     */
    $sql = "DELETE FROM "
            . $this->quoteIdentifier($table, true)
            . (($where) ? " WHERE $where" : '');

    /**
     * Execute the statement and return the number of affected rows
     */
    $stmt = $this->query($sql);
    $result = $stmt->rowCount();
    return $result;
  }

  /**
   * Convert an array, string, or Db_Expr object
   * into a string to put in a WHERE clause.
   *
   * @param mixed $where
   * @return string
   */
  protected function _whereExpr($where) {
    if (empty($where)) {
      return $where;
    }
    if (!is_array($where)) {
      $where = array($where);
    }
    foreach ($where as $cond => &$term) {
      // is $cond an int? (i.e. Not a condition)
      if (is_int($cond)) {
        // $term is the full condition
        if ($term instanceof Db_Expr) {
          $term = $term->__toString();
        }
      } else {
        // $cond is the condition with placeholder,
        // and $term is quoted into the condition
        $term = $this->quoteInto($cond, $term);
      }
      $term = '(' . $term . ')';
    }

    $where = implode(' AND ', $where);
    return $where;
  }

  /**
   * Creates and returns a new Db_Select object for this adapter.
   *
   * @return Db_Select
   */
  public function select() {
    return new Db_Select($this);
  }

  /**
   * Get the fetch mode.
   *
   * @return int
   */
  public function getFetchMode() {
    return $this->_fetchMode;
  }

  /**
   * Fetches all SQL result rows as a sequential array.
   * Uses the current fetchMode for the adapter.
   *
   * @param string|Db_Select $sql  An SQL SELECT statement.
   * @param mixed                 $bind Data to bind into SELECT placeholders.
   * @param mixed                 $fetchMode Override current fetch mode.
   * @return array
   */
  public function fetchAll($sql, $bind = array(), $fetchMode = null) {
    if ($fetchMode === null) {
      $fetchMode = $this->_fetchMode;
    }
    $stmt = $this->query($sql, $bind);
    $result = $stmt->fetchAll($fetchMode);
    return $result;
  }

  /**
   * Fetches the first row of the SQL result.
   * Uses the current fetchMode for the adapter.
   *
   * @param string|Db_Select $sql An SQL SELECT statement.
   * @param mixed $bind Data to bind into SELECT placeholders.
   * @param mixed                 $fetchMode Override current fetch mode.
   * @return array
   */
  public function fetchRow($sql, $bind = array(), $fetchMode = null) {
    if ($fetchMode === null) {
      $fetchMode = $this->_fetchMode;
    }
    $stmt = $this->query($sql, $bind);
    $result = $stmt->fetch($fetchMode);
    return $result;
  }

  /**
   * Fetches all SQL result rows as an associative array.
   *
   * The first column is the key, the entire row array is the
   * value.  You should construct the query to be sure that
   * the first column contains unique values, or else
   * rows with duplicate values in the first column will
   * overwrite previous data.
   *
   * @param string|Db_Select $sql An SQL SELECT statement.
   * @param mixed $bind Data to bind into SELECT placeholders.
   * @return array
   */
  public function fetchAssoc($sql, $bind = array()) {
    $stmt = $this->query($sql, $bind);
    $data = array();
    while ($row = $stmt->fetch(Db::FETCH_ASSOC)) {
      $tmp = array_values(array_slice($row, 0, 1));
      $data[$tmp[0]] = $row;
    }
    return $data;
  }

  /**
   * Fetches the first column of all SQL result rows as an array.
   *
   * @param string|Db_Select $sql An SQL SELECT statement.
   * @param mixed $bind Data to bind into SELECT placeholders.
   * @return array
   */
  public function fetchCol($sql, $bind = array()) {
    $stmt = $this->query($sql, $bind);
    $result = $stmt->fetchAll(Db::FETCH_COLUMN, 0);
    return $result;
  }

  /**
   * Fetches all SQL result rows as an array of key-value pairs.
   *
   * The first column is the key, the second column is the
   * value.
   *
   * @param string|Db_Select $sql An SQL SELECT statement.
   * @param mixed $bind Data to bind into SELECT placeholders.
   * @return array
   */
  public function fetchPairs($sql, $bind = array()) {
    $stmt = $this->query($sql, $bind);
    $data = array();
    while ($row = $stmt->fetch(Db::FETCH_NUM)) {
      $data[$row[0]] = $row[1];
    }
    return $data;
  }

  /**
   * Fetches the first column of the first row of the SQL result.
   *
   * @param string|Db_Select $sql An SQL SELECT statement.
   * @param mixed $bind Data to bind into SELECT placeholders.
   * @return string
   */
  public function fetchOne($sql, $bind = array()) {
    $stmt = $this->query($sql, $bind);
    $result = $stmt->fetchColumn(0);
    return $result;
  }


  /**
   * Safely quotes a value for an SQL statement.
   *
   * If an array is passed as the value, the array values are quoted
   * and then returned as a comma-separated string.
   *
   * @param mixed $value The value to quote.
   * @param mixed $type  OPTIONAL the SQL datatype name, or constant, or null.
   * @return mixed An SQL-safe quoted value (or string of separated values).
   */
  public function quote($value, $type = null) {
    $this->_connect();

    if ($value instanceof Db_Select) {
      return '(' . $value->assemble() . ')';
    }

    if ($value instanceof Db_Expr) {
      return $value->__toString();
    }

    if (is_array($value)) {
      foreach ($value as &$val) {
        $val = $this->quote($val, $type);
      }
      return implode(', ', $value);
    }

    if ($type !== null && array_key_exists($type = strtoupper($type), $this->_numericDataTypes)) {
      $quotedValue = '0';
      switch ($this->_numericDataTypes[$type]) {
        case Db::INT_TYPE: // 32-bit integer
          $quotedValue = (string) intval($value);
          break;
        case Db::BIGINT_TYPE: // 64-bit integer
          // ANSI SQL-style hex literals (e.g. x'[\dA-F]+')
          // are not supported here, because these are string
          // literals, not numeric literals.
          if (preg_match('/^(
                          [+-]?                  # optional sign
                          (?:
                            0[Xx][\da-fA-F]+     # ODBC-style hexadecimal
                            |\d+                 # decimal or octal, or MySQL ZEROFILL decimal
                            (?:[eE][+-]?\d+)?    # optional exponent on decimals or octals
                          )
                        )/x',
                          (string) $value, $matches)) {
            $quotedValue = $matches[1];
          }
          break;
        case Db::FLOAT_TYPE: // float or decimal
          $quotedValue = sprintf('%F', $value);
      }
      return $quotedValue;
    }

    return $this->_quote($value);
  }

  /**
   * Quotes a value and places into a piece of text at a placeholder.
   *
   * The placeholder is a question-mark; all placeholders will be replaced
   * with the quoted value.   For example:
   *
   * <code>
   * $text = "WHERE date < ?";
   * $date = "2005-01-02";
   * $safe = $sql->quoteInto($text, $date);
   * // $safe = "WHERE date < '2005-01-02'"
   * </code>
   *
   * @param string  $text  The text with a placeholder.
   * @param mixed   $value The value to quote.
   * @param string  $type  OPTIONAL SQL datatype
   * @param integer $count OPTIONAL count of placeholders to replace
   * @return string An SQL-safe quoted value placed into the original text.
   */
  public function quoteInto($text, $value, $type = null, $count = null) {
    if ($count === null) {
      return str_replace('?', $this->quote($value, $type), $text);
    } else {
      while ($count > 0) {
        if (strpos($text, '?') !== false) {
          $text = substr_replace($text, $this->quote($value, $type), strpos($text, '?'), 1);
        }
        --$count;
      }
      return $text;
    }
  }

  /**
   * Quotes an identifier.
   *
   * Accepts a string representing a qualified indentifier. For Example:
   * <code>
   * $adapter->quoteIdentifier('myschema.mytable')
   * </code>
   * Returns: "myschema"."mytable"
   *
   * Or, an array of one or more identifiers that may form a qualified identifier:
   * <code>
   * $adapter->quoteIdentifier(array('myschema','my.table'))
   * </code>
   * Returns: "myschema"."my.table"
   *
   * The actual quote character surrounding the identifiers may vary depending on
   * the adapter.
   *
   * @param string|array|Db_Expr $ident The identifier.
   * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
   * @return string The quoted identifier.
   */
  public function quoteIdentifier($ident, $auto=false) {
    return $this->_quoteIdentifierAs($ident, null, $auto);
  }

  /**
   * Quote a column identifier and alias.
   *
   * @param string|array|Db_Expr $ident The identifier or expression.
   * @param string $alias An alias for the column.
   * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
   * @return string The quoted identifier and alias.
   */
  public function quoteColumnAs($ident, $alias, $auto=false) {
    return $this->_quoteIdentifierAs($ident, $alias, $auto);
  }

  /**
   * Quote a table identifier and alias.
   *
   * @param string|array|Db_Expr $ident The identifier or expression.
   * @param string $alias An alias for the table.
   * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
   * @return string The quoted identifier and alias.
   */
  public function quoteTableAs($ident, $alias = null, $auto = false) {
    return $this->_quoteIdentifierAs($ident, $alias, $auto);
  }

  /**
   * Quote an identifier and an optional alias.
   *
   * @param string|array|Db_Expr $ident The identifier or expression.
   * @param string $alias An optional alias.
   * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
   * @param string $as The string to add between the identifier/expression and the alias.
   * @return string The quoted identifier and alias.
   */
  protected function _quoteIdentifierAs($ident, $alias = null, $auto = false, $as = ' AS ') {
    if ($ident instanceof Db_Expr) {
      $quoted = $ident->__toString();
    } elseif ($ident instanceof Db_Select) {
      $quoted = '(' . $ident->assemble() . ')';
    } else {
      if (is_string($ident)) {
        $ident = explode('.', $ident);
      }
      if (is_array($ident)) {
        $segments = array();
        foreach ($ident as $segment) {
          if ($segment instanceof Db_Expr) {
            $segments[] = $segment->__toString();
          } else {
            $segments[] = $this->_quoteIdentifier($segment, $auto);
          }
        }
        if ($alias !== null && end($ident) == $alias) {
          $alias = null;
        }
        $quoted = implode('.', $segments);
      } else {
        $quoted = $this->_quoteIdentifier($ident, $auto);
      }
    }
    if ($alias !== null) {
      $quoted .= $as . $this->_quoteIdentifier($alias, $auto);
    }
    return $quoted;
  }

  /**
   * Quote an identifier.
   *
   * @param  string $value The identifier or expression.
   * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
   * @return string        The quoted identifier and alias.
   */
  protected function _quoteIdentifier($value, $auto=false) {
    if ($auto === false || $this->_autoQuoteIdentifiers === true) {
      $q = $this->getQuoteIdentifierSymbol();
      return ($q . str_replace("$q", "$q$q", $value) . $q);
    }
    return $value;
  }

  /**
   * Returns the symbol the adapter uses for delimited identifiers.
   *
   * @return string
   */
  public function getQuoteIdentifierSymbol() {
    return '"';
  }

  /**
   * Return the most recent value from the specified sequence in the database.
   * This is supported only on RDBMS brands that support sequences
   * (e.g. Oracle, PostgreSQL, DB2).  Other RDBMS brands return null.
   *
   * @param string $sequenceName
   * @return string
   */
  public function lastSequenceId($sequenceName) {
    return null;
  }

  /**
   * Generate a new value from the specified sequence in the database, and return it.
   * This is supported only on RDBMS brands that support sequences
   * (e.g. Oracle, PostgreSQL, DB2).  Other RDBMS brands return null.
   *
   * @param string $sequenceName
   * @return string
   */
  public function nextSequenceId($sequenceName) {
    return null;
  }

  /**
   * Helper method to change the case of the strings used
   * when returning result sets in FETCH_ASSOC and FETCH_BOTH
   * modes.
   *
   * This is not intended to be used by application code,
   * but the method must be public so the Statement class
   * can invoke it.
   *
   * @param string $key
   * @return string
   */
  public function foldCase($key) {
    switch ($this->_caseFolding) {
      case Db::CASE_LOWER:
        $value = strtolower((string) $key);
        break;
      case Db::CASE_UPPER:
        $value = strtoupper((string) $key);
        break;
      case Db::CASE_NATURAL:
      default:
        $value = (string) $key;
    }
    return $value;
  }

  /**
   * called when object is getting serialized
   * This disconnects the DB object that cant be serialized
   *
   * @throws Db_Adapter_Exception
   * @return array
   */
  public function __sleep() {
    if ($this->_allowSerialization == false) {
      /** @see Db_Adapter_Exception */
      require_once 'Zend/Db/Adapter/Exception.php';
      throw new Db_Adapter_Exception(get_class($this) . " is not allowed to be serialized");
    }
    $this->_connection = false;
    return array_keys(array_diff_key(get_object_vars($this), array('_connection' => false)));
  }

  /**
   * called when object is getting unserialized
   *
   * @return void
   */
  public function __wakeup() {
    if ($this->_autoReconnectOnUnserialize == true) {
      $this->getConnection();
    }
  }

  /**
     * Creates a PDO DSN for the adapter from $this->_config settings.
     *
     * @return string
     */
    protected function _dsn()
    {
        // baseline of DSN parts
        $dsn = $this->_config;

        // don't pass the username, password, charset, persistent and driver_options in the DSN
        unset($dsn['username']);
        unset($dsn['password']);
        unset($dsn['options']);
        unset($dsn['charset']);
        unset($dsn['persistent']);
        unset($dsn['driver_options']);

        // use all remaining parts in the DSN
        foreach ($dsn as $key => $val) {
            $dsn[$key] = "$key=$val";
        }

        return $this->_pdoType . ':' . implode(';', $dsn);
    }

    /**
     * Creates a PDO object and connects to the database.
     *
     * @return void
     * @throws Zend_Db_Adapter_Exception
     */
    protected function _connect()
    {
        // if we already have a PDO object, no need to re-connect.
        if ($this->_connection) {
            return;
        }

        // get the dsn first, because some adapters alter the $_pdoType
        $dsn = $this->_dsn();

        // check for PDO extension
        if (!extension_loaded('pdo')) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception('The PDO extension is required for this adapter but the extension is not loaded');
        }

        // check the PDO driver is available
        if (!in_array($this->_pdoType, PDO::getAvailableDrivers())) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception('The ' . $this->_pdoType . ' driver is not currently installed');
        }

        // create PDO connection
        $q = $this->_profiler->queryStart('connect', Zend_Db_Profiler::CONNECT);

        // add the persistence flag if we find it in our config array
        if (isset($this->_config['persistent']) && ($this->_config['persistent'] == true)) {
            $this->_config['driver_options'][PDO::ATTR_PERSISTENT] = true;
        }

        try {
            $this->_connection = new PDO(
                $dsn,
                $this->_config['username'],
                $this->_config['password'],
                $this->_config['driver_options']
            );

            $this->_profiler->queryEnd($q);

            // set the PDO connection to perform case-folding on array keys, or not
            $this->_connection->setAttribute(PDO::ATTR_CASE, $this->_caseFolding);

            // always use exceptions.
            $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        } catch (PDOException $e) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception($e->getMessage(), $e->getCode(), $e);
        }

    }

    /**
     * Test if a connection is active
     *
     * @return boolean
     */
    public function isConnected()
    {
        return ((bool) ($this->_connection instanceof PDO));
    }

    /**
     * Force the connection to close.
     *
     * @return void
     */
    public function closeConnection()
    {
        $this->_connection = null;
    }

    /**
     * Prepares an SQL statement.
     *
     * @param string $sql The SQL statement with placeholders.
     * @param array $bind An array of data to bind to the placeholders.
     * @return PDOStatement
     */
    public function prepare($sql)
    {
        $this->_connect();
        $stmtClass = $this->_defaultStmtClass;
        if (!class_exists($stmtClass)) {
            require_once 'Zend/Loader.php';
            Zend_Loader::loadClass($stmtClass);
        }
        $stmt = new $stmtClass($this, $sql);
        $stmt->setFetchMode($this->_fetchMode);
        return $stmt;
    }

    /**
     * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column.
     *
     * As a convention, on RDBMS brands that support sequences
     * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence
     * from the arguments and returns the last id generated by that sequence.
     * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method
     * returns the last value generated for such a column, and the table name
     * argument is disregarded.
     *
     * On RDBMS brands that don't support sequences, $tableName and $primaryKey
     * are ignored.
     *
     * @param string $tableName   OPTIONAL Name of table.
     * @param string $primaryKey  OPTIONAL Name of primary key column.
     * @return string
     */
    public function lastInsertId($tableName = null, $primaryKey = null)
    {
        $this->_connect();
        return $this->_connection->lastInsertId();
    }

   
    /**
     * Executes an SQL statement and return the number of affected rows
     *
     * @param  mixed  $sql  The SQL statement with placeholders.
     *                      May be a string or Zend_Db_Select.
     * @return integer      Number of rows that were modified
     *                      or deleted by the SQL statement
     */
    public function exec($sql)
    {
        if ($sql instanceof Zend_Db_Select) {
            $sql = $sql->assemble();
        }

        try {
            $affected = $this->getConnection()->exec($sql);

            if ($affected === false) {
                $errorInfo = $this->getConnection()->errorInfo();
                /**
                 * @see Zend_Db_Adapter_Exception
                 */
                require_once 'Zend/Db/Adapter/Exception.php';
                throw new Zend_Db_Adapter_Exception($errorInfo[2]);
            }

            return $affected;
        } catch (PDOException $e) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Quote a raw string.
     *
     * @param string $value     Raw string
     * @return string           Quoted string
     */
    protected function _quote($value)
    {
        if (is_int($value) || is_float($value)) {
            return $value;
        }
        $this->_connect();
        return $this->_connection->quote($value);
    }

    /**
     * Begin a transaction.
     */
    protected function _beginTransaction()
    {
        $this->_connect();
        $this->_connection->beginTransaction();
    }

    /**
     * Commit a transaction.
     */
    protected function _commit()
    {
        $this->_connect();
        $this->_connection->commit();
    }

    /**
     * Roll-back a transaction.
     */
    protected function _rollBack() {
        $this->_connect();
        $this->_connection->rollBack();
    }

    /**
     * Set the PDO fetch mode.
     *
     * @todo Support FETCH_CLASS and FETCH_INTO.
     *
     * @param int $mode A PDO fetch mode.
     * @return void
     * @throws Zend_Db_Adapter_Exception
     */
    public function setFetchMode($mode)
    {
        //check for PDO extension
        if (!extension_loaded('pdo')) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception('The PDO extension is required for this adapter but the extension is not loaded');
        }
        switch ($mode) {
            case PDO::FETCH_LAZY:
            case PDO::FETCH_ASSOC:
            case PDO::FETCH_NUM:
            case PDO::FETCH_BOTH:
            case PDO::FETCH_NAMED:
            case PDO::FETCH_OBJ:
                $this->_fetchMode = $mode;
                break;
            default:
                /**
                 * @see Zend_Db_Adapter_Exception
                 */
                require_once 'Zend/Db/Adapter/Exception.php';
                throw new Zend_Db_Adapter_Exception("Invalid fetch mode '$mode' specified");
                break;
        }
    }

    /**
     * Check if the adapter supports real SQL parameters.
     *
     * @param string $type 'positional' or 'named'
     * @return bool
     */
    public function supportsParameters($type)
    {
        switch ($type) {
            case 'positional':
            case 'named':
            default:
                return true;
        }
    }

    /**
     * Retrieve server version in PHP style
     *
     * @return string
     */
    public function getServerVersion()
    {
        $this->_connect();
        try {
            $version = $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
        } catch (PDOException $e) {
            // In case of the driver doesn't support getting attributes
            return null;
        }
        $matches = null;
        if (preg_match('/((?:[0-9]{1,2}\.){1,3}[0-9]{1,2})/', $version, $matches)) {
            return $matches[1];
        } else {
            return null;
        }
    }
}
